/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package dtd.to.xml;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Class that converts DTD Elements and Attributes to XmlSchema
 * @author silvia
 * @author Mato Majdis
 * @author tomas
 */
public class XSDConverter {

    private Map<String, List<XSDAttribute>> map;
    private int deep = 0; //na meranie urovne zanorenia rekurzie 

    /**
     * Constructor for Class XSDConverter
     */
    public XSDConverter() {

        map = new HashMap<>();
    }

    /**
     * Method convert
     * Creates XMLSchema relevant for input DTD document
     * Input DTD is instance of Class DTDParser
     * 
     * @param dtd Input DTD
     * @return XML Schema
     */
    public XMLSchema convert(DTDParser dtd){
        this.attributesConvert(dtd.getAttributes());                  //namapuje atributy
        
        XMLSchema outputSchema= new XMLSchema();                      //vytvori sa nova prazdna schema           
        outputSchema.setHead(createHead());                           //ulozi sa do nej hlavicka
        outputSchema.setElements(createElements(dtd.getElements()));  //vytvorenie jednotlivych elementov schemy
                
        return outputSchema;
    }
    
    /**
     * Converts all attributes from DTD format to XSD and puts it in map, where 
     * key is name of Element and value is list of attributes.
     * @param attributes attributes to convert
     */
    public void attributesConvert(List<DTDAttribute> attributes) {                       //prevadza vsetky atributy
        XSDAttribute tempAttribute = null;
        for (DTDAttribute att : attributes) {                                            // iteruje sa cez vsetky atributy co stofko vyparsoval v parsery
            if ((map.get(att.getElementName())) == null) {                               // ak v mape este nie element ku ktoremu patri atribut
                map.put(att.getElementName(), new ArrayList<XSDAttribute>());            // tak sa tam vytvori novy zaznam
                tempAttribute = attConvert(att);                                         //atribut sa prevedie z dtd formy na xsd
                map.get(att.getElementName()).add(tempAttribute);                        //a prida 
            } else {                                                                     //ak tam je 
                List<XSDAttribute> temp = new ArrayList<>(map.get(att.getElementName()));//tak sa vytvori pomocny list do ktoreho sa pridaju hodnoty co tam boli predtym(aby sa to neprepisalo)
                tempAttribute = attConvert(att);                                         //prevedenie
                temp.add(tempAttribute);                                                 //pridanie do pomocneho listu
                map.put(att.getElementName(), temp);                                     //pridanie listu do mapy
            }            

        }

    }
    /**
     * Converts attribute from DTD to XSD
     * @param dtdAtt attribute to convert
     * @return converted attribute
     */
    public XSDAttribute attConvert(DTDAttribute dtdAtt) {                         //prevadza jeden atribut

        if (dtdAtt == null) {
            throw new IllegalArgumentException("DTD Attribute is null.");
        }

        String xsdInfo = null;

        XSDAttribute xsdAtt = new XSDAttribute();

        xsdAtt.setName(dtdAtt.getAttributeName());                                //nastavenie mena
        String removedSpaces = dtdAtt.getOtherInfo().trim();                      //odstaranenie medzier zo zaciatku a konca
        String[] otherInfo = removedSpaces.split(" ", 3);                         //rozdelenia podla medzier na najviac 3 casti
        switch (otherInfo.length) {                                               //podla dlzky vzniknuteho pola 
            case 3:                                                               //typ, hodnota = #FIXED,hodnota pri fixed
                xsdInfo = valueFixedConvert(otherInfo, dtdAtt.getAttributeName());//prevod infa
                break;
            case 2:                                                               //typ, hodnota
                xsdInfo = valueOtherConvert(otherInfo, dtdAtt.getAttributeName());//prevod infa
                break;
            default:                                                              //daco zle
                throw new IllegalArgumentException("Wrong number of argument.");
        }
        xsdAtt.setXsdInfo(xsdInfo);                                               //priradenie vzniknuteho infa
        return xsdAtt;
    }
    /**
     * Converts attribute in case it contains #FIXED
     * @param info dtd info
     * @param name attribute name
     * @return converted attribute
     */
    public String valueFixedConvert(String[] info, String name) {                 //prevod v pripade ze je tam fixed hodnota
        String concat;
        
        concat = "<xs:attribute name=\"" + name +"\" fixed=" + info[2] + " type=" + getXSDType(info[0]) + "/>";
        return concat;
    }
    /**
     * Converts attribute in case it does not contain #FIXED
     * @param info dtd info
     * @param name attribute name
     * @return converted attribute
     */
    public String valueOtherConvert(String[] info, String name) { // prevod v pripade ze tam nie je fixed hodnota
        String concat;

        concat = "<xs:attribute name=\"" + name + "\" ";
        if (info[1].contains("\"")) {                            //ak su tam uvodzovky je tam hned dana default hodnota
            concat += "default=" + info[1];
        } else {                                                 // ak nie tak tam je bud required alebo implied
            concat += "use=" + getXSDUse(info[1]);               // tu sa to prevedie 
        }

        if (info[0].contains("(")) {                            //ak typ obsahuje (, tak je to enum 
            concat += ">" + System.lineSeparator();
            concat += createSimpleType(info[0]);                //treba vytvorit simple type
        } else {
            concat += " type=" + getXSDType(info[0]);          //ak je to nieco ine tak sa to tu prevedie
            concat += "/>";
        }

        return concat;
    }
    /**
     * creates simpleType 
     * @param info info to convert
     * @return xmlschema simpleType
     */
    public String createSimpleType(String info) {
        String concat;

        concat = "<xs:simpleType>" + System.lineSeparator();
        concat += "<xs:restriction base=\"xs:NMTOKEN\">" + System.lineSeparator(); //enum je vzdy typ NMTOKEN
        String enumeration;
        Pattern pattern = Pattern.compile("\\w+");                                 // hladaju sa slova (Stofko|Mato|Fefe) :D (najde sa : Stofko, Mato, Fefe) ignoruju sa ( a |
        Matcher matcher = pattern.matcher(info);
        while (matcher.find()) {                                                   //ked sa najdu
            enumeration = matcher.group();                                         //tak sa ulozia  
            concat += createEnumeration(enumeration);                              // a prevedu
        }
        concat += "</xs:restriction>" + System.lineSeparator();
        concat += "</xs:simpleType>" + System.lineSeparator();
        concat += "</xs:attribute>";
        return concat;
    }
    /**
     * creates xmlSchema enumeration type
     * @param enumeration string to convert
     * @return xmlSchema enum
     */
    public String createEnumeration(String enumeration) {
        String value = "<xs:enumeration value=\"" + enumeration + "\"/>" + System.lineSeparator();
        return value;
    }
    /**
     * converts DTD information about use to xmlschema format
     * @param type
     * @return  type in xmlSchema format
     */
    public String getXSDUse(String type) {// prevediet use parameter
        switch (type) {
            case "#REQUIRED":
                return "\"required\"";
            case "#IMPLIED":
                return "\"optional\"";
            default:
                return null;
        }
    }
    /**
     * Converts DTD type to xmlschema type
     * @param info
     * @return xsd typ
     */
    public String getXSDType(String info) { // prevedie typ parameter
        switch (info) {
            case "CDATA":
                return "\"xs:string\"";
            case "ID":
                return "\"xs:ID\"";
            case "IDREF":
                return "\"xs:IDREF\"";
            case "IDREFS":
                return "\"xs:IDREFS\"";
            case "NMTOKEN":
                return "\"xs:NMTOKEN\"";
            case "NMTOKENS":
                return "\"xs:NMTOKENS\"";
            case "ENTITY":
                return "\"xs:ENTITY\"";
            case "ENTITIES":
                return "\"xs:ENTITIES\"";
            default:
                return null;
        }
    }
    
    /**
     * Method createHead
     * Returns header of Schema
     * 
     * @return Schema header as String
     */
    private String createHead() {
        String head = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>";
        head += System.lineSeparator();
        
        return head;
    }
    
    /**
     * Method createElements
     * Returns List of XML Schema elements created
     * to relevant DTD elements and attributes
     * 
     * @param elements Input DTD elements
     * @return List of XSD Elements
     */
    public List<String> createElements (List<DTDElement> elements) {
        List<String> xsdElements = new ArrayList<>();                                 
        for(DTDElement element : elements) {                                          
            String tempElement = "<xs:element name=\"" + element.getName() + '\"';   
            tempElement += parseType(element);                                        
            xsdElements.add(tempElement);                                            
        }       
        
        return xsdElements;                                                         
    }

    /**
     * Method parseType
     * Converts info from DTD element to XSD element type
     * Returns Type of XSD element as a String
     * 
     * @param element DTD element to create type for
     * @return Type of XSD element as a String
     */
    private String parseType(DTDElement element) {
        String info = element.getInfo();                         
        if(info == null) {                                        
            return null;                                       
        }
        List<XSDAttribute> atts= map.get(element.getName());    
        switch(info) {                                          
            case "EMPTY":                                       
                return emptyType(atts);
            case "ANY":
                return anyType(atts);
            case "#PCDATA":
                return textType(atts);
        }
                                                                
        return complexType(info, atts);                         
    }

    /**
     * Method emptyType
     * If DTDs info contains string "EMPTY"
     * returns empty complexType with attributes inside
     * 
     * @param atts List of attributes to add to complexType
     * @return Empty complexType with attributes
     */
    private String emptyType(List<XSDAttribute> atts) {
        return complexType("EMPTY", atts);
    }

    /**
     * Method anyType
     * If element has no attributes returns element without type
     * Otherwise returns complexType with anyType and attributes
     * 
     * @param atts List of attributes to add to complexType
     * @return Anytype string of element
     */
    private String anyType(List<XSDAttribute> atts) {
        String ct;
        if(atts == null) {
            ct = "/>" + System.lineSeparator() + System.lineSeparator();
        } else {
            ct = complexType("ANY", atts);
        }
        
        return ct;
    }

    /**
     * Method textType
     * Returns text content for element
     * If element has no attributes type is xs:string
     * complexType with simpleContent otherwise
     * 
     * @param atts List of attributes to add to complexType
     * @return Text type for element as a string
     */
    private String textType(List<XSDAttribute> atts) {
        String ct;
        if(atts == null) {
            ct = " type=\"xs:string\"/>" + System.lineSeparator() + System.lineSeparator();
        } else {
            ct = complexType("#SIMPLE", atts);
        }
        
        return ct;
    }
    
    /**
     * Method complexType
     * Creates complexType of XSD element
     * Adds attributes to ComplexType
     * 
     * @param info DTD info
     * @param atts DTD attributes to add
     * @return ComplexType for element as a String
     */
    private String complexType(String info, List<XSDAttribute> atts) {
        String ct = createComplexHead(info);                              
        if(info.equals("#SIMPLE")) {                                      
            ct += "<xs:simpleContent>" + System.lineSeparator();       
            ct += "<xs:extension base=\"xs:string\">"; 
            ct += System.lineSeparator();
        } else {
            if(!info.equals("EMPTY")) {                                  
                ct += addRefs(info);                                   
            }
        }      
        if(atts != null) {                                                 
            for(XSDAttribute att : atts){
                ct += att.getXsdInfo() + System.lineSeparator();
            }
        }
        if(info.equals("#SIMPLE")) {                                     
            ct += "</xs:extension>" + System.lineSeparator();        
            ct+= "</xs:simpleContent>" + System.lineSeparator();
        }
        ct += "</xs:complexType>" + System.lineSeparator();              
        ct += "</xs:element>" + System.lineSeparator();                  
        ct += System.lineSeparator();   
        
        return ct;
    }
    
    /**
     * Method craeteComplexHead
     * Returns base of complexType
     * Defines "mixed" attribute of ComplexType
     * 
     * @param info DTD info
     * @return Header of complexType as a String
     */
    private String createComplexHead(String info) {              
        String cHead = ">" + System.lineSeparator() + "<xs:complexType";
        if(info.indexOf("#PCDATA")!=-1) {
            cHead +=  " mixed=\"true\"";
        }
        cHead += ">" + System.lineSeparator();
        
        return cHead;
    }
    
    /**
     * Method addRefs
     * Returns content of complexType if
     * content are references to other elements
     * 
     * @param info DTD info
     * @return Created construction of  XSD elements refs
     */
    private String addRefs(String info) {
        String refs;
        if(info.equals("ANY")) {                                             
            refs = "<xs:sequence>" + System.lineSeparator();                
            refs += "<xs:any minOccurs=\"0\" maxOccurs=\"unbounded\"/>"; 
            refs += System.lineSeparator();
            refs += "</xs:sequence>" + System.lineSeparator();
        } else {                                                              
            refs = createStructure(info);                                  
        }
        
        return refs;
    }

    /**
     * Method createStructure
     * Convert DTDs info to XSD elements refs
     * If info contains more than 1 element ref
     * will be parsed and recursively called until
     * info contains only 1 element
     * Then creates element reference
     * 
     * @param info DTD info
     * @return XML Schema construction- elements refs
     */
    private String createStructure(String info) {
        deep++;                                                                           
        String structure;                                                                 
        boolean simple = true;                                                            
        if((info.indexOf('|') != -1) || (info.indexOf(',') != -1)) {                        
            simple = false;
            structure = parseByLastChar(info);                                            
        } else {                                                                        
            info = info.replaceAll("[()]", "");                                           
            structure = createElement(info);                                              
        }
        deep--;
        
        return structure;
    }
    
    /**
     * Method parseByLastChar
     * Auxiliary method to parse DTD info
     * 
     * 
     * @param info DTD info
     * @return XML Schema construction- elements refs
     */
    private String parseByLastChar(String info) {
        String structure;
        char lastChar = info.charAt(info.length()-1);
        switch(lastChar) {                                                             
            case '?': case '+': case'*':                                             
                    if(isClosed(info.substring(0, info.length()-1))) {                
                        info= info.substring(1, info.length()-2);                       
                        structure = parseInfo(info, lastChar);
                    } else {
                        structure = parseInfo(info, '0');
                    }
                break;
            case ')':
                    if(isClosed(info)) {
                        info = info.substring(1, info.length()-1);
                    }
                    structure = parseInfo(info, '0');
                break;
            default:
                    structure = parseInfo(info, '0');
                break;
        }
        
        return structure;
    }
    
    /**
     * Method isClosed
     * Checks if input string "info"
     * is closed in brackets
     * 
     * @param info Input term
     * @return True if term is closed in brackets, false otherwise
     */
    private boolean isClosed(String info) {
        boolean closed = true;
        if(info.indexOf('(') == -1) {
            return false;
        }
        int level = 0;
        int length = info.length()-1;
        for(int i = 0; i < length+1; i++) {
            char actualChar = info.charAt(i);
            switch(actualChar) {
                case '(':
                    level++;
                    if((i != 0) && (level == 1)){
                        closed = false;
                    }
                    break;
                case ')':
                    level--;
                    if((i != length) && (level == 0)) {
                        closed = false;
                    }
                    break;
            }
        }
        
        return closed;
    }

    /**
     * Method parseInfo
     * Auxiliary method to parse DTD info
     * Parses input term by "," or "|"
     * Than recursively call createStructure(...) for
     * each parsed part
     * Puts part into sequences / choices
     * 
     * @param info DTD term
     * @param occ Frequency of term occurence ("*","+","?")
     * @return XML Schema construction- elements refs
     */
    private String parseInfo(String info, char occ) {
        String terms = "";
        String[] occurency = createOccurency(occ);
        String minO = occurency[0];
        String maxO = occurency[1];        
        int i = 0;
        int counter = 0;
        int brLevel = 0;
        char actualChar;
        String parsed = "";
        String toClose = "</xs:sequence>" + System.lineSeparator();
        while(!(i == info.length())) { 
            actualChar = info.charAt(i);
            switch(actualChar) {
                case '(':
                    brLevel++;
                    parsed += String.valueOf(actualChar);
                    break;
                case ')':
                    brLevel--;
                    parsed += String.valueOf(actualChar);
                    break;
                case '|': case ',':
                    if(brLevel == 0){
                        String termType = "sequence";
                        if(actualChar =='|'){
                            termType = "choice";
                            toClose = "</xs:choice>" + System.lineSeparator();
                        }
                        counter++;
                        if(counter > 1) {
                            terms += (createStructure(parsed));
                        } else {
                            String term= "<xs:" + termType + " minOccurs=" + minO;
                            term +=" maxOccurs=" + maxO + ">" + System.lineSeparator();
                            terms += (term + createStructure(parsed));
                        }
                        parsed = "";
                    } else {
                        parsed += String.valueOf(actualChar);
                    }
                    break;
                default:
                    parsed += String.valueOf(actualChar);
                    break;                      
            }
            i++;
        }
        if(counter != 0){
            terms += (createStructure(parsed)) + toClose;
        } else {
            String sequence = "<xs:sequence minOccurs=" + minO; 
            sequence += " maxOccurs=" + maxO + ">" + System.lineSeparator();
            terms += (sequence + createStructure(parsed) + toClose);
        }
        
        return terms;
    }
    
    /**
     * Method createOccurency
     * Converts DTD occurency ("*","+","?") to XSD form
     * 
     * @param occ Occurency character in DTD form ("*","+","?")
     * @return Occurency term in XSD form as a String
     */
    private String[] createOccurency(char occ) {
        String occurency[] = {"\"1\"", "\"1\""} ;
        switch(occ) {
            case '?':
                occurency[0] = "\"0\"";
                break;
            case '+':
                occurency[1] = "\"unbounded\"";
                break;
            case '*':
                occurency[0] = "\"0\"";
                occurency[1] = "\"unbounded\"";
                break;
        }
        
        return occurency;
    }

    /**
     * Method createElement
     * Creates reference to element
     * Adds occurency for element
     * 
     * @param info DTD info about element to create reference for
     * @return XSD reference to element as a String
     */
    private String createElement(String info) {
        if(info.contains("#PCDATA")) {
            return "";
        }
        char lastChar = info.charAt(info.length()-1);
        if((lastChar == '*') || (lastChar == '?') || (lastChar == '+')) {
            info = info.substring(0, info.length()-1);
        }
        String[] occurency;
        occurency = createOccurency(lastChar);
        String minO = occurency[0];
        String maxO = occurency[1];
        String element = "";
        if(deep == 1) {
            element = "<xs:sequence>" + System.lineSeparator();
        }
        element += "<xs:element ref=\"" + info + "\" minOccurs=" + minO + " maxOccurs=" + maxO + "/>";
        element += System.lineSeparator();
        if(deep == 1){
            element += "</xs:sequence>" + System.lineSeparator();
        }
        
        return element;
    }
}
